HEX
Server: Apache/2.4.68 (Debian)
System: Linux as-cs-widget-demo-us-central1 6.1.0-44-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.164-1 (2026-03-09) x86_64
User: root (0)
PHP: 8.2.32
Disabled: NONE
Upload Files
File: /var/www/kevin-demo/wp-content/plugins/allspice/includes/content-badges.php
<?php
// includes/content-badges.php
//
// Lock badges for whole-content-gated posts, on the PUBLISHER'S listings (home/archive/
// search/related/widget cards) - never inside the Allspice widget, and never on the current
// singular post's own hero image.
//
// DOCUMENTED CHOICE: the badge is RETAINED for authorized members in listings - a premium
// item stays visibly premium, which is the recommendation this module follows.
//
// V1 hooks post_thumbnail_html only. Themes that build card images without
// get_the_post_thumbnail()/the_post_thumbnail() bypass this filter and will need a small
// theme-specific adapter later; a broad client-side DOM scanner is not built; add a theme-specific adapter instead.

if (!defined('ABSPATH')) exit;

/* Synced lock_badges settings, from the ONE normalizer (schema v2). Legacy configs carry no
   lock_badges object; they keep the historical default-on behavior so already-deployed sites
   don't lose badges mid-migration. v2 configs require enabled === true exactly. */
function allspice_lock_badge_config(): array {
    if (function_exists('allspice_memberships_normalized')) {
        $n = allspice_memberships_normalized();
        $badges = $n['lock_badges'];
        if ($n['legacy'] === true) {
            /* Old schema never shipped lock_badges: retain the prior always-on default. */
            $badges['enabled'] = true;
        }
        return $badges;
    }
    return ['enabled' => true, 'label' => 'Members', 'background_color' => null];
}

function allspice_lock_badge_enabled(): bool {
    return (bool)apply_filters('allspice_lock_badge_enabled', allspice_lock_badge_config()['enabled'] === true);
}

function allspice_lock_badge_label(): string {
    $label = trim((string)allspice_lock_badge_config()['label']);
    if ($label === '') $label = 'Members';
    return (string)apply_filters('allspice_lock_badge_label', $label);
}

/* Sanitized (strict color sanitizer) or null -> the stylesheet default. */
function allspice_lock_badge_background_color() {
    return allspice_lock_badge_config()['background_color'];
}

/* 'top-right' (default) or 'bottom-right'. */
function allspice_lock_badge_position(): string {
    $pos = (string)apply_filters('allspice_lock_badge_position', 'top-right');
    return $pos === 'bottom-right' ? 'bottom-right' : 'top-right';
}

function allspice_lock_badge_class(): string {
    return trim((string)apply_filters('allspice_lock_badge_class', ''));
}

add_filter('post_thumbnail_html', 'allspice_lock_badge_thumbnail', 20, 5);
function allspice_lock_badge_thumbnail($html, $post_id = 0, $post_thumbnail_id = 0, $size = '', $attr = '') {
    if (!is_string($html) || $html === '' || !allspice_lock_badge_enabled()) return $html;
    /* Fail open with memberships disabled/not ready - no badges from a stale snapshot. */
    if (!function_exists('allspice_gate_memberships_active') || !allspice_gate_memberships_active()) return $html;
    /* Never on the current singular post's primary hero. */
    if (is_singular() && is_main_query() && (int)get_queried_object_id() === (int)$post_id) return $html;
    /* Never twice. */
    if (strpos($html, 'allspice-lock-badge') !== false) return $html;
    /* Only whole-content gates: recipe_card-only and public posts have no rule here. */
    if (!function_exists('allspice_content_gate_for_post')) return $html;
    $rule = allspice_content_gate_for_post((int)$post_id);
    if (!is_array($rule)) return $html;

    $label = allspice_lock_badge_label();
    $classes = 'allspice-lock-badge allspice-lock-badge--' . allspice_lock_badge_position();
    $extra = allspice_lock_badge_class();
    if ($extra !== '') $classes .= ' ' . $extra;
    $badge = '<span class="' . esc_attr($classes) . '">'
        . '<svg aria-hidden="true" viewBox="0 0 24 24" width="12" height="12" focusable="false">'
        . '<path fill="currentColor" d="M12 2a5 5 0 0 0-5 5v3H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8a2 2 0 0 0-2-2h-1V7a5 5 0 0 0-5-5zm-3 8V7a3 3 0 1 1 6 0v3H9z"/></svg>'
        . '<span class="allspice-lock-badge__label" aria-hidden="true">' . esc_html($label) . '</span>'
        . '<span class="screen-reader-text allspice-lock-badge__sr">' . esc_html($label) . ' - members-only content</span>'
        . '</span>';
    /* Wrap image + badge in a positioned span, so any theme link AROUND the thumbnail keeps
       working and the badge anchors to the image box. */
    return '<span class="allspice-lock-badge-wrap">' . $html . $badge . '</span>';
}

/* Badge CSS rides the existing always-loaded gate stylesheet handle. */
add_action('wp_enqueue_scripts', 'allspice_lock_badge_styles', 21);
function allspice_lock_badge_styles(): void {
    if (!function_exists('allspice_gate_memberships_active') || !allspice_gate_memberships_active()) return;
    if (!allspice_lock_badge_enabled()) return;
    /* HOST-THEME ARMOR (live finding, 2026-08-26): the badge renders inside the theme's own
       thumbnail link, where `a span { color/line-height }` rules outrank a single class.
       Every property a theme plausibly styles on links and spans is pinned per element with
       !important - the same per-element armor rule the inline widget uses. */
    wp_add_inline_style('allspice-gate',
        '.allspice-lock-badge-wrap{position:relative;display:inline-block;max-width:100%}'
        . '.allspice-lock-badge{position:absolute;display:inline-flex!important;align-items:center!important;gap:4px;'
        . 'padding:3px 8px;border-radius:999px;background:'
        . (allspice_lock_badge_background_color() !== null ? allspice_lock_badge_background_color() : 'rgba(20,22,28,.82)')
        . ';color:#fff!important;'
        . 'font-size:11px!important;font-weight:600;line-height:1!important;letter-spacing:.02em;'
        . 'text-decoration:none!important;pointer-events:none;z-index:2;transition:opacity .18s ease}'
        . '.allspice-lock-badge__label{color:#fff!important;font-size:11px!important;line-height:1!important;display:block}'
        . '.allspice-lock-badge--top-right{top:8px;right:8px}'
        . '.allspice-lock-badge--bottom-right{bottom:8px;right:8px}'
        . '.allspice-lock-badge svg{display:block;width:12px;height:12px;flex:0 0 auto;color:#fff!important}'
        /* Applied by the loader script below while the badge is anchored to a still-collapsed
           lazy image box; removed on load/error. Without JS it never applies (badges visible). */
        . '.allspice-lock-badge--waiting{opacity:0}'
        . '.allspice-lock-badge .screen-reader-text{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}'
    );
}

/*
 * A lazy thumbnail with no reserved height collapses the wrap the badge anchors to, so the
 * badge sat at the wrong spot until the image arrived and then jumped into place. The badge
 * defaults VISIBLE (no-JS keeps badges); this only HIDES badges whose image is genuinely
 * still loading and reveals them (with a short fade) the moment it lands or fails.
 */
add_action('wp_footer', 'allspice_lock_badge_loading_script', 5);
function allspice_lock_badge_loading_script(): void {
    if (!function_exists('allspice_gate_memberships_active') || !allspice_gate_memberships_active()) return;
    if (!allspice_lock_badge_enabled()) return;
    echo '<script>(function(){var m=function(w){var i=w.querySelector("img"),b=w.querySelector(".allspice-lock-badge");'
        . 'if(!i||!b)return;'
        . 'if(i.complete&&i.naturalWidth>0){b.classList.remove("allspice-lock-badge--waiting");return;}'
        . 'b.classList.add("allspice-lock-badge--waiting");'
        . 'var d=function(){b.classList.remove("allspice-lock-badge--waiting");};'
        . 'i.addEventListener("load",d,{once:true});i.addEventListener("error",d,{once:true});};'
        . 'var boot=function(){document.querySelectorAll(".allspice-lock-badge-wrap").forEach(m);};'
        . 'if(document.readyState==="loading"){document.addEventListener("DOMContentLoaded",boot);}else{boot();}'
        . '})();</script>' . "\n";
}